
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: SimpleField.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar;

import org.dellroad.sidekar.util.SidekarException;
import org.dellroad.stuff.string.ParseContext;

/**
 * A {@link Field} that can be directly translated to and from a simple Java type and can
 * be encoded in string form. {@link SimpleField}s do not contain nested fields.
 *
 * @param <T> Java type used to represent this field's value
 */
public abstract class SimpleField<T> extends Field<T> {

    private final Class<? extends T> runtimeType;

    /**
     * Constructor.
     *
     * @param name name of this field
     * @param entity entity of which this field is a member
     * @param type Java type that represents this field's value
     */
    SimpleField(String name, Entity<?> entity, Class<? extends T> type) {
        this(name, entity, type, type);
    }

    /**
     * Constructor used by {@link ArrayType} to track runtime array types.
     */
    SimpleField(String name, Entity<?> entity, Class<? extends T> type, Class<? extends T> runtimeType) {
        super(name, entity, type);
        this.runtimeType = runtimeType;
    }

    /**
     * Used by {@link ArrayType} to track runtime array types.
     */
    Class<? extends T> getRuntimeType() {
        return this.runtimeType;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Class<? extends T> getType() {
        return (Class<? extends T>)super.getType();
    }

    /**
     * Cast the object to this field's Java type.
     *
     * <p>
     * This implementation casts the object to the type given in the constructor.
     *
     * @throws ClassCastException if the object is not of type T
     */
    public T cast(Object obj) {
        return this.getType().cast(obj);
    }

    /**
     * Compare two field values for equality.
     */
    public abstract boolean equals(T value1, T value2);

    /**
     * @throws DetachedItemException {@inheritDoc}
     * @throws IllegalArgumentException {@inheritDoc}
     * @throws IllegalStateException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     * @throws org.dellroad.sidekar.util.SidekarException {@inheritDoc}
     */
    @Override
    public T get(Item<?> item) {
        return item.getSidekarEntity().readField(item, this);
    }

    /**
     * Set the value of this field in the given item.
     *
     * @param item an instance of the {@link Entity} (or any sub-{@link Entity} containing this field)
     * @param value new value for this field
     * @throws DetachedItemException if item does not exist in the database
     * @throws IllegalArgumentException if item does not contain this field
     * @throws IllegalStateException if the database containing this field's entity has been closed
     * @throws SidekarException if an underlying database error occurs
     */
    public void set(Item<?> item, T value) {
        item.getSidekarEntity().writeField(item, this, value);
    }

    /**
     * Encode a field value as a string.
     *
     * <p>
     * The resulting string must satisfy these constraints:
     * <ul>
     * <li>Does not start with a closing curly brace</li>
     * <li>Does not start or end with a whitespace character</li>
     * <li>Can be decoded correctly even if it is immediately followed by a comma or whitespace character</li>
     * </ul>
     *
     * @param value the value to encode
     * @return encoded string suitable for input to {@link #fromString}
     * @see #fromString
     */
    public abstract String toString(T value);

    /**
     * Decode a field value from a string.
     * This is a convenience method that simply invokes:
     *  <blockquote>
     *  <code>fromString(new ParseContext(string))</code>
     *  </blockquote>
     *
     * @param string encoded string returned from {@link #toString}, possibly followed by other stuff
     * @return the original value
     * @throws IllegalArgumentException if the string is invalid
     * @see #fromString(ParseContext)
     * @see #toString
     */
    public final T fromString(String string) {
        return fromString(new ParseContext(string));
    }

    /**
     * Decode a field value from a string. The string may possibly have additional characters appended; if
     * so, these characters must not be valid continuations of the encoded value (i.e., no ambiguity allowed).
     *
     * @param context parse context with encoded string returned from {@link #toString}, possibly followed by other stuff
     * @return the original value
     * @throws IllegalArgumentException if the input is invalid
     * @see #fromString(String)
     * @see #toString
     */
    public abstract T fromString(ParseContext context);

    /**
     * Get the {@link SimpleField} type corresponding to the given Java class.
     *
     * @return the corresponding {@link SimpleField} class
     * @throws IllegalArgumentException if no such field type exists
     */
    @SuppressWarnings("unchecked")
    public static Class<? extends SimpleField<?>> getFieldType(Class<?> javaType) {
        try {
            return (Class<? extends SimpleField<?>>)FieldType.forJavaType(javaType).getFieldType().asSubclass(SimpleField.class);
        } catch (ClassCastException e) {
            throw new IllegalArgumentException("no known simple field type for " + javaType);
        }
    }
}

